/**
 * Copyright 2025 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as assert from 'assert';
import type { Part } from 'genkit';
import { describe, it } from 'node:test';

import { BetaRunner } from '../src/runner/beta.js';
import { createMockAnthropicClient } from './mocks/anthropic-client.js';

describe('BetaRunner.toAnthropicMessageContent', () => {
  function createRunner() {
    return new BetaRunner({
      name: 'anthropic/claude-3-5-haiku',
      client: createMockAnthropicClient(),
      cacheSystemPrompt: false,
    });
  }

  it('converts PDF media parts into document blocks', () => {
    const runner = createRunner();
    const part: Part = {
      media: {
        contentType: 'application/pdf',
        url: 'data:application/pdf;base64,UEsDBAoAAAAAAD',
      },
    };

    const result = (runner as any).toAnthropicMessageContent(part);

    assert.strictEqual(result.type, 'document');
    assert.ok(result.source);
    assert.strictEqual(result.source.type, 'base64');
    assert.strictEqual(result.source.media_type, 'application/pdf');
    assert.ok(result.source.data);
  });

  it('throws when tool request ref is missing', () => {
    const runner = createRunner();
    const part: Part = {
      toolRequest: {
        name: 'do_something',
        input: { foo: 'bar' },
      },
    };

    assert.throws(() => {
      (runner as any).toAnthropicMessageContent(part);
    }, /Tool request ref is required/);
  });

  it('maps tool request with ref into tool_use block', () => {
    const runner = createRunner();
    const part: Part = {
      toolRequest: {
        ref: 'tool-123',
        name: 'do_something',
        input: { foo: 'bar' },
      },
    };

    const result = (runner as any).toAnthropicMessageContent(part);

    assert.strictEqual(result.type, 'tool_use');
    assert.strictEqual(result.id, 'tool-123');
    assert.strictEqual(result.name, 'do_something');
    assert.deepStrictEqual(result.input, { foo: 'bar' });
  });

  it('throws when tool response ref is missing', () => {
    const runner = createRunner();
    const part: Part = {
      toolResponse: {
        name: 'do_something',
        output: 'done',
      },
    };

    assert.throws(() => {
      (runner as any).toAnthropicMessageContent(part);
    }, /Tool response ref is required/);
  });

  it('maps tool response into tool_result block containing text response', () => {
    const runner = createRunner();
    const part: Part = {
      toolResponse: {
        name: 'do_something',
        ref: 'tool-abc',
        output: 'done',
      },
    };

    const result = (runner as any).toAnthropicMessageContent(part);

    assert.strictEqual(result.type, 'tool_result');
    assert.strictEqual(result.tool_use_id, 'tool-abc');
    assert.deepStrictEqual(result.content, [{ type: 'text', text: 'done' }]);
  });

  it('should handle WEBP image data URLs', () => {
    const runner = createRunner();
    const part: Part = {
      media: {
        contentType: 'image/webp',
        url: '',
      },
    };

    const result = (runner as any).toAnthropicMessageContent(part);

    assert.strictEqual(result.type, 'image');
    assert.strictEqual(result.source.type, 'base64');
    assert.strictEqual(result.source.media_type, 'image/webp');
    assert.strictEqual(result.source.data, 'AAA');
  });

  it('should prefer data URL content type over media.contentType for WEBP', () => {
    const runner = createRunner();
    const part: Part = {
      media: {
        // Even if contentType says PNG, data URL says WEBP - should use WEBP
        contentType: 'image/png',
        url: '',
      },
    };

    const result = (runner as any).toAnthropicMessageContent(part);

    assert.strictEqual(result.type, 'image');
    assert.strictEqual(result.source.type, 'base64');
    // Key fix: should use data URL type (webp), not contentType (png)
    assert.strictEqual(result.source.media_type, 'image/webp');
    assert.strictEqual(result.source.data, 'AAA');
  });

  it('should throw helpful error for text/plain in toAnthropicMessageContent', () => {
    const runner = createRunner();
    const part: Part = {
      media: {
        contentType: 'text/plain',
        url: 'data:text/plain;base64,AAA',
      },
    };

    assert.throws(
      () => {
        (runner as any).toAnthropicMessageContent(part);
      },
      (error: Error) => {
        return (
          error.message.includes('Text files should be sent as text content') &&
          error.message.includes('text:')
        );
      }
    );
  });

  it('should throw helpful error for text/plain with remote URL', () => {
    const runner = createRunner();
    const part: Part = {
      media: {
        contentType: 'text/plain',
        url: 'https://example.com/file.txt',
      },
    };

    assert.throws(
      () => {
        (runner as any).toAnthropicMessageContent(part);
      },
      (error: Error) => {
        return (
          error.message.includes('Text files should be sent as text content') &&
          error.message.includes('text:')
        );
      }
    );
  });

  it('should throw helpful error for text/plain in tool response', () => {
    const runner = createRunner();
    const part: Part = {
      toolResponse: {
        ref: 'call_123',
        name: 'get_file',
        output: {
          url: 'data:text/plain;base64,AAA',
          contentType: 'text/plain',
        },
      },
    };

    assert.throws(
      () => {
        (runner as any).toAnthropicToolResponseContent(part);
      },
      (error: Error) => {
        return error.message.includes(
          'Text files should be sent as text content'
        );
      }
    );
  });
});
/**
 * Copyright 2025 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Anthropic } from '@anthropic-ai/sdk';
import { mock } from 'node:test';

describe('BetaRunner', () => {
  it('should map all supported Part shapes to beta content blocks', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });

    const exposed = runner as any;

    const textPart = exposed.toAnthropicMessageContent({
      text: 'Hello',
    } as any);
    assert.deepStrictEqual(textPart, { type: 'text', text: 'Hello' });

    const pdfPart = exposed.toAnthropicMessageContent({
      media: {
        url: 'data:application/pdf;base64,JVBERi0xLjQKJ',
        contentType: 'application/pdf',
      },
    } as any);
    assert.strictEqual(pdfPart.type, 'document');

    const imagePart = exposed.toAnthropicMessageContent({
      media: {
        url: '',
        contentType: 'image/png',
      },
    } as any);
    assert.strictEqual(imagePart.type, 'image');

    const toolUsePart = exposed.toAnthropicMessageContent({
      toolRequest: {
        ref: 'tool1',
        name: 'get_weather',
        input: { city: 'NYC' },
      },
    } as any);
    assert.deepStrictEqual(toolUsePart, {
      type: 'tool_use',
      id: 'tool1',
      name: 'get_weather',
      input: { city: 'NYC' },
    });

    const toolResultPart = exposed.toAnthropicMessageContent({
      toolResponse: {
        ref: 'tool1',
        name: 'get_weather',
        output: 'Sunny',
      },
    } as any);
    assert.strictEqual(toolResultPart.type, 'tool_result');

    assert.throws(() => exposed.toAnthropicMessageContent({} as any));
  });

  it('should convert beta stream events to Genkit Parts', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });

    const exposed = runner as any;
    const textPart = exposed.toGenkitPart({
      type: 'content_block_start',
      index: 0,
      content_block: { type: 'text', text: 'hi' },
    } as any);
    assert.deepStrictEqual(textPart, { text: 'hi' });

    const serverToolEvent = {
      type: 'content_block_start',
      index: 0,
      content_block: {
        type: 'server_tool_use',
        id: 'toolu_test',
        name: 'myTool',
        input: { foo: 'bar' },
        server_name: 'srv',
      },
    } as any;
    const toolPart = exposed.toGenkitPart(serverToolEvent);
    assert.deepStrictEqual(toolPart, {
      text: '[Anthropic server tool srv/myTool] input: {"foo":"bar"}',
      custom: {
        anthropicServerToolUse: {
          id: 'toolu_test',
          name: 'srv/myTool',
          input: { foo: 'bar' },
        },
      },
    });

    const deltaPart = exposed.toGenkitPart({
      type: 'content_block_delta',
      index: 0,
      delta: { type: 'thinking_delta', thinking: 'hmm' },
    } as any);
    assert.deepStrictEqual(deltaPart, { reasoning: 'hmm' });

    const ignored = exposed.toGenkitPart({ type: 'message_stop' } as any);
    assert.strictEqual(ignored, undefined);
  });

  it('should throw on unsupported mcp tool stream events', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });

    const exposed = runner as any;
    assert.throws(
      () =>
        exposed.toGenkitPart({
          type: 'content_block_start',
          index: 0,
          content_block: {
            type: 'mcp_tool_use',
            id: 'toolu_unsupported',
            input: {},
          },
        }),
      /server-managed tool block 'mcp_tool_use'/
    );
  });

  it('should map beta stop reasons correctly', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });

    const finishReason = runner['fromBetaStopReason'](
      'model_context_window_exceeded'
    );
    assert.strictEqual(finishReason, 'length');

    const pauseReason = runner['fromBetaStopReason']('pause_turn');
    assert.strictEqual(pauseReason, 'stop');
  });

  it('should execute streaming calls and surface errors', async () => {
    const streamError = new Error('stream failed');
    const mockClient = createMockAnthropicClient({
      streamChunks: [
        {
          type: 'content_block_start',
          index: 0,
          content_block: { type: 'text', text: 'hi' },
        } as any,
      ],
      streamErrorAfterChunk: 1,
      streamError,
    });

    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });
    const sendChunk = mock.fn();
    await assert.rejects(async () =>
      runner.run({ messages: [] } as any, {
        streamingRequested: true,
        sendChunk,
        abortSignal: new AbortController().signal,
      })
    );
    assert.strictEqual(sendChunk.mock.calls.length, 1);

    const abortController = new AbortController();
    abortController.abort();
    await assert.rejects(async () =>
      runner.run({ messages: [] } as any, {
        streamingRequested: true,
        sendChunk: () => {},
        abortSignal: abortController.signal,
      })
    );
  });

  it('should throw when tool refs are missing in message content', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });
    const exposed = runner as any;

    assert.throws(() =>
      exposed.toAnthropicMessageContent({
        toolRequest: {
          name: 'get_weather',
          input: {},
        },
      } as any)
    );

    assert.throws(() =>
      exposed.toAnthropicMessageContent({
        toolResponse: {
          name: 'get_weather',
          output: 'ok',
        },
      } as any)
    );

    assert.throws(() =>
      exposed.toAnthropicMessageContent({
        media: {
          url: 'data:image/png;base64,',
          contentType: undefined,
        },
      } as any)
    );
  });

  it('should build request bodies with optional config fields', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-3-5-haiku',
      client: mockClient as Anthropic,
      cacheSystemPrompt: true,
    }) as any;

    const request = {
      messages: [
        {
          role: 'system',
          content: [{ text: 'You are helpful.' }],
        },
        {
          role: 'user',
          content: [{ text: 'Tell me a joke' }],
        },
      ],
      config: {
        maxOutputTokens: 128,
        topK: 4,
        topP: 0.65,
        temperature: 0.55,
        stopSequences: ['DONE'],
        metadata: { user_id: 'beta-user' },
        tool_choice: { type: 'tool', name: 'get_weather' },
        thinking: { enabled: true, budgetTokens: 2048 },
      },
      tools: [
        {
          name: 'get_weather',
          description: 'Returns the weather',
          inputSchema: { type: 'object' },
        },
      ],
    } satisfies any;

    const body = runner.toAnthropicRequestBody(
      'claude-3-5-haiku',
      request,
      true
    );

    assert.strictEqual(body.model, 'claude-3-5-haiku');
    assert.ok(Array.isArray(body.system));
    assert.strictEqual(body.max_tokens, 128);
    assert.strictEqual(body.top_k, 4);
    assert.strictEqual(body.top_p, 0.65);
    assert.strictEqual(body.temperature, 0.55);
    assert.deepStrictEqual(body.stop_sequences, ['DONE']);
    assert.deepStrictEqual(body.metadata, { user_id: 'beta-user' });
    assert.deepStrictEqual(body.tool_choice, {
      type: 'tool',
      name: 'get_weather',
    });
    assert.strictEqual(body.tools?.length, 1);
    assert.deepStrictEqual(body.thinking, {
      type: 'enabled',
      budget_tokens: 2048,
    });

    const streamingBody = runner.toAnthropicStreamingRequestBody(
      'claude-3-5-haiku',
      request,
      true
    );
    assert.strictEqual(streamingBody.stream, true);
    assert.ok(Array.isArray(streamingBody.system));
    assert.deepStrictEqual(streamingBody.thinking, {
      type: 'enabled',
      budget_tokens: 2048,
    });

    const disabledBody = runner.toAnthropicRequestBody(
      'claude-3-5-haiku',
      {
        messages: [],
        config: {
          thinking: { enabled: false },
        },
      } satisfies any,
      false
    );
    assert.deepStrictEqual(disabledBody.thinking, { type: 'disabled' });
  });

  it('should concatenate multiple text parts in system message', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-3-5-haiku',
      client: mockClient as Anthropic,
    }) as any;

    const request = {
      messages: [
        {
          role: 'system',
          content: [
            { text: 'You are a helpful assistant.' },
            { text: 'Always be concise.' },
            { text: 'Use proper grammar.' },
          ],
        },
        { role: 'user', content: [{ text: 'Hi' }] },
      ],
      output: { format: 'text' },
    } satisfies any;

    const body = runner.toAnthropicRequestBody(
      'claude-3-5-haiku',
      request,
      false
    );

    assert.strictEqual(
      body.system,
      'You are a helpful assistant.\n\nAlways be concise.\n\nUse proper grammar.'
    );
  });

  it('should concatenate multiple text parts in system message with caching', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-3-5-haiku',
      client: mockClient as Anthropic,
    }) as any;

    const request = {
      messages: [
        {
          role: 'system',
          content: [
            { text: 'You are a helpful assistant.' },
            { text: 'Always be concise.' },
          ],
        },
        { role: 'user', content: [{ text: 'Hi' }] },
      ],
      output: { format: 'text' },
    } satisfies any;

    const body = runner.toAnthropicRequestBody(
      'claude-3-5-haiku',
      request,
      true
    );

    assert.ok(Array.isArray(body.system));
    assert.deepStrictEqual(body.system, [
      {
        type: 'text',
        text: 'You are a helpful assistant.\n\nAlways be concise.',
        cache_control: { type: 'ephemeral' },
      },
    ]);
  });

  it('should throw error if system message contains media', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-3-5-haiku',
      client: mockClient as Anthropic,
    }) as any;

    const request = {
      messages: [
        {
          role: 'system',
          content: [
            { text: 'You are a helpful assistant.' },
            {
              media: {
                url: '',
                contentType: 'image/png',
              },
            },
          ],
        },
        { role: 'user', content: [{ text: 'Hi' }] },
      ],
      output: { format: 'text' },
    } satisfies any;

    assert.throws(
      () => runner.toAnthropicRequestBody('claude-3-5-haiku', request, false),
      /System messages can only contain text content/
    );
  });

  it('should throw error if system message contains tool requests', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-3-5-haiku',
      client: mockClient as Anthropic,
    }) as any;

    const request = {
      messages: [
        {
          role: 'system',
          content: [
            { text: 'You are a helpful assistant.' },
            { toolRequest: { name: 'getTool', input: {}, ref: '123' } },
          ],
        },
        { role: 'user', content: [{ text: 'Hi' }] },
      ],
      output: { format: 'text' },
    } satisfies any;

    assert.throws(
      () => runner.toAnthropicRequestBody('claude-3-5-haiku', request, false),
      /System messages can only contain text content/
    );
  });

  it('should throw error if system message contains tool responses', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-3-5-haiku',
      client: mockClient as Anthropic,
    }) as any;

    const request = {
      messages: [
        {
          role: 'system',
          content: [
            { text: 'You are a helpful assistant.' },
            { toolResponse: { name: 'getTool', output: {}, ref: '123' } },
          ],
        },
        { role: 'user', content: [{ text: 'Hi' }] },
      ],
      output: { format: 'text' },
    } satisfies any;

    assert.throws(
      () => runner.toAnthropicRequestBody('claude-3-5-haiku', request, false),
      /System messages can only contain text content/
    );
  });

  it('should throw for unsupported mcp tool use blocks', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });
    const exposed = runner as any;

    assert.throws(
      () =>
        exposed.fromBetaContentBlock({
          type: 'mcp_tool_use',
          id: 'toolu_unknown',
          input: {},
        }),
      /server-managed tool block 'mcp_tool_use'/
    );
  });

  it('should convert additional beta content block types', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });

    const thinkingPart = (runner as any).fromBetaContentBlock({
      type: 'thinking',
      thinking: 'pondering',
      signature: 'sig_456',
    });
    assert.deepStrictEqual(thinkingPart, {
      reasoning: 'pondering',
      custom: { anthropicThinking: { signature: 'sig_456' } },
    });

    const redactedPart = (runner as any).fromBetaContentBlock({
      type: 'redacted_thinking',
      data: '[redacted]',
    });
    assert.deepStrictEqual(redactedPart, {
      custom: { redactedThinking: '[redacted]' },
    });

    const toolPart = (runner as any).fromBetaContentBlock({
      type: 'tool_use',
      id: 'toolu_x',
      name: 'plainTool',
      input: { value: 1 },
    });
    assert.deepStrictEqual(toolPart, {
      toolRequest: {
        ref: 'toolu_x',
        name: 'plainTool',
        input: { value: 1 },
      },
    });

    const serverToolPart = (runner as any).fromBetaContentBlock({
      type: 'server_tool_use',
      id: 'srv_tool_1',
      name: 'serverTool',
      input: { arg: 'value' },
      server_name: 'srv',
    });
    assert.deepStrictEqual(serverToolPart, {
      text: '[Anthropic server tool srv/serverTool] input: {"arg":"value"}',
      custom: {
        anthropicServerToolUse: {
          id: 'srv_tool_1',
          name: 'srv/serverTool',
          input: { arg: 'value' },
        },
      },
    });

    const warnMock = mock.method(console, 'warn', () => {});
    const fallbackPart = (runner as any).fromBetaContentBlock({
      type: 'mystery',
    });
    assert.deepStrictEqual(fallbackPart, { text: '' });
    assert.strictEqual(warnMock.mock.calls.length, 1);
    warnMock.mock.restore();
  });

  it('should map additional stop reasons', () => {
    const mockClient = createMockAnthropicClient();
    const runner = new BetaRunner({
      name: 'claude-test',
      client: mockClient as Anthropic,
    });
    const exposed = runner as any;

    const refusal = exposed.fromBetaStopReason('refusal');
    assert.strictEqual(refusal, 'other');

    const unknown = exposed.fromBetaStopReason('something-new');
    assert.strictEqual(unknown, 'other');

    const nullReason = exposed.fromBetaStopReason(null);
    assert.strictEqual(nullReason, 'unknown');
  });
});
